# 第五章 后端轮播图管理

通过前面几个章节的知识铺垫、开发环境搭建以及准备工作,从本章节开始就将正式开始跟着作者一起敲代码来实现各种业务功能。如果在前面章节中你遇到了困难,不要灰心,不要气馁,因为后面还会有更多困难,但这都不是问题,挫折越多,收获越多,还有作者一路伴你同行。

# 查询所有轮播图

首先我们来实现一个可以查询所有轮播图的接口。在控制层新建一个Banner控制器,右键controller/v1目录,在弹出菜单中选择New——PHP Class,在name输入框这里输入Banner然后回车

这里开发工具已经帮我们自动生成了一些基础代码,接下来我们来为这个控制器实现一个getBanners()方法,这个方法用于获取所有轮播图。

<?php

namespace app\api\controller\v1;

class Banner
{
    public function getBanners()
    {

    }

}
1
2
3
4
5
6
7
8
9
10
11
12

前面准备工作环节我们已经把原型APP的数据库表导入到了数据库中做为素材,其中就包括轮播图相关的数据表,那么现在我们就来尝试获取这部分数据。

在正式进行获取轮播图数据之前,我们先打开phpMyAdmin并点击zerg数据库,点击后会显示该数据库下所有已存在的表,你会发现有bannerbanner_item两张表。那么我们该读取哪张表呢?答案是两张都要读。banner和banner_item是一种关联关系,一个banner对应多个banner_item,专业点来说就是表的一对多关系。

为什么要这么设计呢?一个轮播图里面有多少个要轮播的内容是不确定的,假如用一张表实现,你设置5个字段分别用于存放轮播图的内容,那么如果有一天你需要轮播的内容为6个,你只能通过加字段的方式,后面还可能会有7个8个等等,你要不停的加字段。然后你又不是每个轮播图都有那么多的内容需要轮播,那多出来的字段就等于浪费了空间。

那么如何定义两张表是关联的关系呢?我们分别打开两张表,你会发现在banner_item表中,有一个banner_id字段,这个字段的值对应了banner表中的某一条记录的id字段的值。比如现在banner_item表中就有4条记录,他们的banner_id都为1,代表了这几个item都是属于banner表中id为1的banner。当然光是这样定义还是不够的,我们还需要借助TP5框架中的模型关联来实现关联查询,但表字段的预先定义是前提。了解了表的关系和字段内容之后,我们马上通过代码来实现查询所有轮播图的功能。

要查询数据库,写SQL语句?不存在的,我们用模型!首先需要在模型层分别新建BannerBannerItem模型。右键api/model目录,在弹出菜单中选择New——PHP Class,在name输入框这里输入Banner然后回车,再重复同样操作BannerItem模型。

要让模型对应到数据库中的某张表,我还需要让模型类继承TP5框架的Model类。

<?php


namespace app\api\model;


use think\Model;

class Banner extends Model
{

}
1
2
3
4
5
6
7
8
9
10
11
12
<?php


namespace app\api\model;


use think\Model;

class BannerItem extends Model
{

}
1
2
3
4
5
6
7
8
9
10
11
12

模型的类名必须与数据库表的表名一致才能对应上,类名必须是驼峰形式且大写开头。

模型定义好了之后,回到我们的Banner控制器下,在前面创建的getBanners()中我们来调用模型进行查询,这里在调用Banner模型的时候IDE(开发工具)已经智能感知到了模型类:

这里我们选择显示app\api\model这个命名空间下的类:

<?php


namespace app\api\controller\v1;


class Banner
{
    public function getBanners()
    {
        // 调用了TP5框架数据库模型的select()方法
        // 该方法用于查询表的所有记录
        $result = \app\api\model\Banner::select();
        // 返回查询结果给前端
        return $result;
    }

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

但是回车后发现这代码有点惨不忍睹,这是因为模型类名和控制器的类名重名了,所以自动帮我们以完整的命名空间的方式调用,为了兼顾代码的可读性和美观,我们需要稍微处理下:

<?php


namespace app\api\controller\v1;
// use这个模型类,并用as指定一个别名
use app\api\model\Banner as BannerModel;

class Banner
{
    public function getBanners()
    {
        $result = BannerModel::select();
        return $result;
    }

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

完美!然后我们怎么请求这个接口以验证是否符合我们的预期呢?前面我们说到了,要请求接口,必须要有地址,事不宜迟,我们来给这个接口定义一个路由地址。

双击IDE左边资源管理中根目录下的route\route.php,可以看到里面已经存在很多路由,TP5框架定义路由的方法是使用内置的Route类:

Route::group('', function () {
    Route::group('cms', function () {
        // CMS管理相关的路由规则
        // 内容省略。。。。
    });
    Route::group('v1', function () {
        // 业务接口相关的路由规则
        // 内容省略。。。。
    });
})->middleware(['Auth','ReflexValidate'])->allowCrossDomain();
1
2
3
4
5
6
7
8
9
10

按照lin-cms-tp5框架的推荐,我们在v1目录下定义了控制器,同样的,我这里也在v1这个路由分组下定义路由规则,v1分组下原有的图书管理相关路由可以删除掉:

Route::group('', function () {
    Route::group('cms', function () {
        // CMS管理相关的路由规则
        // 内容省略。。。。
    });
    Route::group('v1', function () {
        // 业务接口相关的路由规则
        Route::group('banner',function(){
            Route::get('','api/v1.Banner/getBanners');
        });
    });
})->middleware(['Auth','ReflexValidate'])->allowCrossDomain();
1
2
3
4
5
6
7
8
9
10
11
12

为了整个路由定义内容有结构性和归纳性,我们在v1这个路由分组下,使用Route::group()方法再嵌套一个路由分组,分组名为banner

分组名会做为url地址的一部分,从最顶层的分组名往下拼接,这里的顶层分组名为空,表示/,即域名根地址,接着是v1,即/v1,再往下是我们的banner,即最终的规则是/v1/banner

在分组里面,我们定义一条请求类型为GET的路由,get()方法这里我们传递两个参数,第一个是路由规则,第二个是该路由要执行的操作,这里第一个参数我们设置为空,表示当访问了web服务器地址/v1/banner这个地址的时候,就去执行api/v1.Banner/getBanners方法(即我们在控制器中定义的getBanners())。

现在控制器、模型、路由都定义好了,检查下我们的PHP内置web服务器是否依然在运行状态,如果没有,可以直接在PhpStorm中按下Alt+F12,这时候会在IDE里打开命令行工具(非常方便的一个功能,不用开多一个窗口),默认打开的路径就已经是项目根目录了,直接运行php think run启动PHP内置web服务器。

接下来我们就要来试着访问这个接口,看看结果是否符合我们的预期。双击运行我们前面安装了的Postman,点击左侧的+New Collection,起个名字叫lin-cms-tp5(名字请随意),然后你就会看到下面多出来一个像文件夹一样的东西

把鼠标移动到刚刚创建的这个文件夹上,右下角会有三个小点,点击然后选择Add Request,会弹出一个窗口,按下图配置:

最后点击右下角的save to 轮播图,最终效果

以后我们每一个接口就按照这种方式,分门别类的设置好,方便我们后面使用。点击刚刚我们创建的请求,地址栏输入我们PHP内置web服务器的地址加上我们刚刚在路由配置文件里面配置的路由规则,然后点击Send按钮:

没有报错,接口正常返回了数据,但是数据的内容不大对,这里只返回了banner表中的内容,轮播图的具体内容banner_item没有返回。这是因为我们在查询的时候,只查询了Banner模型,那这里我们是不是要根据查询结果里面的id再去查询Banner_item模型呢?可以,但没必要!前面说了,我们有关联查询这种东西!要实现关联查询,除了表字段的预先定义,还需要在模型类中定义两个模型的关系。回到我们的代码中,打开Banner模型类,加入以下代码:


<?php


namespace app\api\model;


use think\Model;

class Banner extends Model
{

    // 方法名可以自定义,通过在模型类里面定义一个方法,声明关联关系,这个方法返回被关联模型的实例
    public function items()
    {
        // 调用了模型实例的hasMany()方法,这个方法定义了当前模型与被关联模型BannerItem是一种一对多的关系
        // 关联的内容是BannerItem模型里banner_id属性的值与当前模型的id属性的值一致的记录。
        return $this->hasMany('BannerItem','banner_id', 'id');
    }
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

hasMany()方法中,第二个参数默认是当前模型的小写类名+_id,第三个参数默认是id,由于我们在设计表的时候已经考虑到了这点,所以这里可以直接缩写为:return $this->hasMany('BannerItem');读者在开发自己项目的时候也可以利用这一点小技巧。

定义完关联关系之后,回到我们的控制器方法getBanners(),稍作修改:

<?php


namespace app\api\controller\v1;

use app\api\model\Banner as BannerModel;

class Banner
{
    public function getBanners()
    {
        // 在调用select()方法之前,先调用with(),并传入刚刚在模型类中定义的关联方法名。
        $result = BannerModel::with('items')->select();
        return $result;
    }

}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

回到Postman中,再次点击Send,得到结果如下:

[
    {
        "id": 1,
        "name": "首页置顶",
        "description": "首页轮播图",
        "delete_time": null,
        "update_time": null,
        "items": [
            {
                "id": 1,
                "img_id": 65,
                "key_word": "6",
                "type": 1,
                "delete_time": null,
                "banner_id": 1,
                "update_time": null
            },
            {
                "id": 2,
                "img_id": 2,
                "key_word": "25",
                "type": 1,
                "delete_time": null,
                "banner_id": 1,
                "update_time": null
            },
            {
                "id": 3,
                "img_id": 3,
                "key_word": "11",
                "type": 1,
                "delete_time": null,
                "banner_id": 1,
                "update_time": null
            },
            {
                "id": 5,
                "img_id": 1,
                "key_word": "10",
                "type": 1,
                "delete_time": null,
                "banner_id": 1,
                "update_time": null
            }
        ]
    }
]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48

这下子轮播图和所属元素就都查询出来了,原来的结果中多出了一个items的键,它的值是一个数组,数组的内容就是banner_item表中的内容。

不使用模型关联查询,意味着你需要先foreach一次select()的结果,然后在循环体里面根据结果里的id去查询BannerItemModel,还要拼装数组,这个过程既费时又很低效。

思考题:上海实行了垃圾分类,市民们苦于分不清干垃圾、湿垃圾,技术人员小明决定开发一个APP让大家可以查询每个分类下面都有什么垃圾,请问这时候小明要如何设计数据库表。

到这里我们的接口并没有开发完成,仔细观察返回结果item键的值,里面并没有我们想象中该有的图片地址,只有一个img_id,这样的接口数据,前端是没办法得知你这个轮播图下面到底是什么图片内容。通过这个字段名的观察,我们很容易猜到了这里也是需要使用关联查询,是的,没错,banner_item表与image表也是存在表的关联关系。由于img_id是在banner_item表定义的,所以我要在BannerItem模型中去定义关联关系:


<?php


namespace app\api\model;


use think\Model;

class BannerItem extends Model
{
    public function img()
    {
        // 调用了模型实例的belongsTo()方法,这个方法定义了当前模型与被关联模型Image是一种相对关系
        // 关联的内容是BannerItem模型里img_id属性的值与Image模型的id属性的值一致的记录。
        return $this->belongsTo('Image','img_id');
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

这里我们用belongsTo()来定义模型关系。在TP5框架中,有个hasOne()的方法,通过调整参数,可以实现同样的效果,但是这里为什么不选择呢?我们可以看到image表中是没有任何类似xxx_id的字段的,因为这个表是公共的,我们有很多地方会到用到。对于image表来说,里面的图片是用于哪个功能模块的它并不关心,不能与某个表具体绑死在一起,也就是说image表的记录只能是一种被关联的角色,所以我们在其他表中通过定义img_id字段来关联这个image表中记录,使用belongsTo()来定义模型关联并查询。这样做的好处就是第一,语义化明确,has,拥有谁,belongs,属于谁,对于BannerItem模型来说,img_id是属于Image模型的,更符合语义化;第二就是前面说的可以保持image表的独立通用。

官方文档关于这个这个并没有太多的解释,称之为相对关联,这里权当参考。很多小伙伴一直很懵逼hasXXX()和belongsXXX()的区别和使用场景,其实就是看你的数据库表设计时的关联字段放到哪个表里还有你是从哪个表发起查询,比如说这里我们从image表反过来查询banner_item,那这时候在Image模型里定义关联的声明的时候就是要用hasMany()或者hasOne()了。另外这里第二个参数不能省略,省略的话会以banner_item_id为默认参数,查询结果不会发生变化。

这里我们还没有创建Image模型,右键api/model目录,在弹出菜单中选择New——PHP Class,在name输入框这里输入Image然后回车,同样需要继承Model类:

<?php


namespace app\api\model;


use think\Model;

class Image extends Model
{

}

1
2
3
4
5
6
7
8
9
10
11
12
13

增加了BannerItem模型的关联定义和Image模型之后,再次回到控制器方法getBanners,稍作修改:

<?php


namespace app\api\controller\v1;

use app\api\model\Banner as BannerModel;

class Banner
{
    public function getBanners()
    {
        // items代表了BannerItem模型的实例,.img的作用就是告知框架,还要调用items实例里的关联模型,即我们刚刚定义的img()
        $result = BannerModel::with('items.img')->select();
        return $result;
    }

}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

回到Postman,直接Send,得到如下结果:

[
    {
        "id": 1,
        "name": "首页置顶",
        "description": "首页轮播图",
        "delete_time": null,
        "update_time": null,
        "items": [
            {
                "id": 1,
                "img_id": 65,
                "key_word": "6",
                "type": 1,
                "delete_time": null,
                "banner_id": 1,
                "update_time": null,
                "img": {
                    "id": 65,
                    "url": "/banner-4a.png",
                     "from": 1,
                    "delete_time": null
                }
            },
            {
                "id": 2,
                "img_id": 2,
                "key_word": "25",
                "type": 1,
                "delete_time": null,
                "banner_id": 1,
                "update_time": null,
                "img": {
                    "id": 2,
                    "url": "/banner-2a.png",
                     "from": 1,
                    "delete_time": null
                }
            },
           .......省略内容
        ]
    }
]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42

和之前一样,items下数组里的每个元素都多出了一个img的键,值就是Image模型的内容。但是这里依然达不到我们预期的效果,imgurl属性的值是一个相对路径,这种url地址提供给前端一样无法显示,在解决这个问题之前我们先搞清楚为什么image表里要这么存储数据。打开phpMyAdmin,找到image这张表,点击上方菜单的结构

在真实业务开发场景中,我们的图片文件并不一定都是放在应用运行所在的服务器,有可能是阿里云、腾讯云,也有可能自己专门架设了一台图片服务器,还可能是由多台图片服务器组成的集群。也就是说这个图片地址有那么一部分是不确定的,所以我们在url字段中只存储确定的那一部分,通过from字段来标识这个图片是存在在哪里,然后我们在模型读取这个记录的时候,去判断这个from的值,根据不同的值来拼接这个url字段里的值。

了解了设计初衷之后,我们就来从代码层面解决这个问题。解决思路就是在数据返回给前端之前,要先加工一下这个url字段,我们可能第一时间会想到就是用foreach循环,循环到了img这个元素,然后判断元素里面from的值,给个对应的url前缀拼接url字段。这样是可行的,但是没必要。我们需要用TP5框架模型内置的获取器这个功能。

官方解释:获取器的作用是对模型实例的(原始)数据做出自动处理。一个获取器对应模型的一个特殊方法(该方法必须为public类型),方法命名规范为:get+字段名(首字母大写)+Attr

因为这里我们需要对Image模型的数据做处理,所以我们在Image模型下定义一个获取器,打开model/Image.php加入以下代码:

<?php

namespace app\api\model;

use think\Model;

class Image extends BaseModel
{
    // 我们要获取url这个字段,获取默认接收两个参数
    // $value是当前这条记录里url字段的值
    // $data是当前记录的完整数据
    public function getUrlAttr($value, $data)
    {
        $finalUrl = $value;
        // 根据表的注释,1来自本地,2来自公网
        if($data['from'] == 1){// 如果来自本地,把本机的存放图片目录的域名地址跟$value拼接
            // 这里我们把本机的存放图片目录的域名地址写到了一个配置文件里。
            // 后续我们可能换了域名或者目录,又或者有其他来源渠道,以配置文件的形式这样以后只需改配置文件而不必改动代码
            // 利用TP5框架内置的助手函数config()快速指定获取配置文件下内容
            $finalUrl = config('setting.img_prefix').$value;
        }
        // 这里如果from不是来自本地,那么存储的会是一个完整的公网访问地址,无需处理
        // 返回处理后的url
        return $finalUrl;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

别忘了创建一个配置文件,在IDE中,右键根目录下的config目录,选择New——PHP File,文件名写setting。创建好后,双击打开,添加如下内容:

<?php
return [
    'img_prefix' => 'http://localhost:8000/images',# 仅做示例
];
1
2
3
4

注:配置文件必须放置在根目录下的config目录

回到我们的Postman中,再次点击Send发送请求:

[
    {
        "id": 1,
        "name": "首页置顶",
        "description": "首页轮播图",
        "delete_time": null,
        "update_time": null,
        "items": [
            {
                "id": 1,
                "img_id": 65,
                "key_word": "6",
                "type": 1,
                "delete_time": null,
                "banner_id": 1,
                "update_time": null,
                "img": {
                    "id": 65,
                    "url": "http://localhost:8000/images/banner-4a.png",
                    "from": 1,
                    "delete_time": null
                }
            }
            .........省略后面重复内容
        ]
    }
]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

这里看到我们的url已经是一个完整的可访问地址了,前端在调用这个接口后,就可以直接通过读取这个url地址显示图片了。

获取器在日常开发中是很常用的功能。除了以上的场景,通常我们对一些诸如状态值的存储并不是直接存储文字内容而是数字(枚举值),比如订单状态,0代表未支付,1代表已支付。直接存储字符串内容会增加字段的占用空间(不同编码中文占用的空间不一,一般在2~4个字节),数字通过设定表的字段类型可以让其占用空间仅为1个字节,这时候我们也可以通过获取器来实现枚举值转具体文字含义。

到这里,查询所有轮播图接口的开发就接近尾声了,最后我们来为这个接口做一些小小的优化。这里我们接口返回的数据中,有些字段前端其实并不需要或者不应该在接口数据中返回。作为后端接口开发者,我们要从性能和安全的角度上对接口的返回数据进行控制。也许你又想着foreach了,但是这里同样不需要。TP5框架的模型类已经为我们提供了很方便的方法,这里以Image模型为例,IDE中打开我们的Image模型类,定义一个类的成员变量$hidden:

<?php


namespace app\api\model;


use think\Model;

class Image extends Model
{
    protected $hidden = ['delete_time','from'];

    public function getUrlAttr($value, $data)
    {
        ....
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

通过给$hidden变量传递一个数组,数组里是要隐藏的字段名,这里我们传递fromdelete_time。然后回到Postman再次发送一次请求;

[
    {
        "id": 1,
        "name": "首页置顶",
        "description": "首页轮播图",
        "delete_time": null,
        "update_time": null,
        "items": [
            {
                "id": 1,
                "img_id": 65,
                "key_word": "6",
                "type": 1,
                "delete_time": null,
                "banner_id": 1,
                "update_time": null,
                "img": {
                    "id": 65,
                    "url": "http://localhost:8000/public/images/banner-4a.png",
                }
            }
            .........省略后面重复内容
        ]
    }
]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

这里可以看到,接口返回数据中,已经没有了fromdelete_time字段。读者可以尝试在BannerBannerItem中定义$hidden成员变量实现精简优化返回数据的内容。

扩展知识:
模型关联,并不一定需要表中存在idxxx_id字段,在定义模型关联的时候,可以自由利用两张表中的某个字段来实现,但是不推荐这么做,因为可读性太差。通过前面的实战我们可以发现,虽然我们在一开始并不了解整个项目的情况,但是通过字段名就可以大概判断出字段的用途,一个字段同时承载多个用途也不是正确的表字段设计方式。模型关联按我的理解就是在代码层面去实现关联表与表的关系,这就赋予了很多灵活性,而且一定程度上让开发者可以更少的接触数据库的东西(开发者需要关心数据库的东西,但是这个关心有点过度了)。而与之对应的,就是外键(也叫外键约束或者实体外键)了。在传统项目的数据库中,普遍使用外键来约束表与表的关联关系,直接从数据库层面定义死了。由于有了外键的存在,你在代码层面就必须保证表与表之间数据的一致性,不然会导致异常错误。同时在项目迭代过程中需要对涉及到存在外键的表进行调整,整个工作将会变得很复杂。这两种定义表关联关系没有说谁好谁坏,要看具体业务的应用场景,像银行这种,系统N年不更新,强调数据一致性,那么用外键是比较合适的;互联网行业,项目经常性迭代、重构,使用模型关联则更合适。

# 新增轮播图

通过前面的学习,我们实现了所有轮播图的查询,接下来我们就来学习动手开发一个新增轮播图的接口。还是那个套路,我们要先创建一个控制器,由于这个接口同样属于轮播图的范畴,所以我们还是在上一小结创建的Banner控制器类中新增一个控制器方法即可。使用开发工具打开项目根目录下的/application/api/controller/v1/Banner.php,在getBanners()下面新增一个方法:

<?php


namespace app\api\controller\v1;

use app\api\model\Banner as BannerModel;

class Banner
{
    public function getBanners()
    {
        $result = BannerModel::with('items.img')->select();
        return $result;
    }

    public function addBanner()
    {
        
    }

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

既然是要新增的一个轮播图,那后端就肯定要知道新增的内容,首先我们就需要接收一下前端传递过来的数据,这里我们使用TP5框架内置Request类来实现:

<?php


namespace app\api\controller\v1;

use app\api\model\Banner as BannerModel;
use think\facade\Request;

class Banner
{
    .................

    public function addBanner()
    {
        // Request::post()用于获取当前POST类型请求所携带所有参数,结果是一个数组
        // 给post()传递一个字符串则表示获取指定参数名称的值,比如post('id')
        $params = Request::post();
    }

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

这里我们通过Request类的post方法获取了参数并将它赋值给了$params,接下来我们要做的就是把$params里的内容解析并存入数据库中,这里自然就又轮到我们的模型出场了。打开我们上一小结在模型层下创建的Banner模型,我们封装一个模型方法add()

<?php


namespace app\api\model;


use think\Model;

class Banner extends Model
{

    public static function add($parmas)
    {

    }

    public function items()
    {
        return $this->hasMany('BannerItem');
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

add()方法接收一个$params参数,即我们在控制器方法中获取到的前端请求参数。这里我们假设约定本接口需提交的数据内容和格式为:

{
	"name": "首页置顶",
    "description": "首页轮播图",
    "items":[
        {
            "img_id": 65,
            "key_word": "6",
            "type": 1
        },
        {
            "img_id": 65,
            "key_word": "6",
            "type": 1
        }
    ]
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

namedescription是用于banner表的,items的内容则是需要写入banner_item表的。如同前面使用关联查询一样,这里我们新增数据同样使用模型的关联新增

<?php


namespace app\api\model;


use think\Model;

class Banner extends Model
{

    public static function add($parmas)
    {
        // 调用当前模型的静态方法create(),第一个参数为要写入的数据,第二个参数标识仅写入数据表定义的字段数据
        $banner = self::create($parmas, true);
        // 调用关联模型,实现关联写入;saveAll()方法用于批量新增数据
        $banner->items()->saveAll($parmas['items']);

    }

    public function items()
    {
        return $this->hasMany('BannerItem');
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

由于提交过来的$parms参数中同时含有两个表需要插入的内容,所以这里我们在插入banner表数据的时候,给create()方法的第二个参数传了个true,这样模型在插入数据的时候就会自动帮我们过滤掉$params参数中的items那部分数据。插入成功后,create()方法会返回Banner模型的实例,我们利用这个模型实例调用上一小结中定义的items()得到了BannerItem模型,然后调用saveAll()并把$params中items部分的数据传递进去来实现关联新增。

这里可能有点绕,上一小节中我们用items()声明了两个模型的关系,这个方法其实返回的就是BannerItem模型的实例,在这里我们复用了这个方法直接拿到BannerItem模型然后让BannerItem模型调用saveAll()

模型方法封装完成之后,我们回到Banner控制器的addBanner()方法,调用刚刚封装的模型方法:

<?php


namespace app\api\controller\v1;

use app\api\model\Banner as BannerModel;
use think\facade\Request;

class Banner
{
    ...........

    public function addBanner()
    {
        $params = Request::post();
        BannerModel::add($params);
    }

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

控制器方法和模型都已定义完毕,接下来就是定义路由,打开Route.php,同样在banner路由分组下面,定义一条路由规则:

Route::group('', function () {
    Route::group('cms', function () {
        // CMS管理相关的路由规则
        // 内容省略。。。。
    });
    Route::group('v1', function () {
        // 业务接口相关的路由规则
        Route::group('banner',function(){
            Route::get('','api/v1.Banner/getBanners');
            Route::post('','api/v1.Banner/addBanner');

        });
    });
})->middleware(['Auth','ReflexValidate'])->allowCrossDomain();
1
2
3
4
5
6
7
8
9
10
11
12
13
14

这里我们同样定义了一条与查询所有轮播图一样地址的路由,区别就是请求方法为POST,并指定要执行的操作是我们的控制器方法addBanner

万事俱备,来个Postman,打开我们的Postman,点击左边我们上一小结创建出来的文件夹,在展开的目录中再点击轮播图目录名旁边的三个小点,选择Add Request

给这个Request起个名字并保存。

保存完毕后,轮播图目录下就多出了一个新增轮播图的接口了,双击它,地址栏输入完整的API接口地址,请求方法改为POST,这时候还不可以发送这个请求,因为我们还需要利用postman来模拟数据提交。

在下面的输入区域,粘贴以下内容:

{
	"name": "LinCMS",
    "description": "第一个新增的轮播图",
    "items":[
        {
            "img_id": 65,
            "key_word": "6",
            "type": 1
        },
        {
            "img_id": 65,
            "key_word": "6",
            "type": 1
        }
    ]
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

这里我们尝试新增一个包含两个轮播元素的轮播图,点击发送。这时候你会发现没有任何错误信息,是否已经添加成功呢?让我们来验证一下!点击postman左边我们上一小节创建的查询所有轮播图接口,打开后直接发送请求,我们会看到返回结果中,多出了id为2的轮播图,它有两个元素,正是刚刚我们所提交的内容,这说明数据已经顺利插入到数据库中了。

{
    "id": 2,
    "name": "LinCMS",
    "description": "第一个新增的轮播图",
    "delete_time": null,
    "update_time": null,
    "items": [
        {
            "id": 5,
            "img_id": 65,
            "key_word": "6",
            "type": 1,
            "delete_time": null,
            "banner_id": 2,
            "update_time": null,
            "img": {
                "id": 65,
                "url": "http://localhost:8000/public/images/banner-4a.png"
            }
        },
        {
            "id": 6,
            "img_id": 65,
            "key_word": "6",
            "type": 1,
            "delete_time": null,
            "banner_id": 2,
            "update_time": null,
            "img": {
                "id": 65,
                "url": "http://localhost:8000/public/images/banner-4a.png"
            }
        }
    ]
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36

到这里,我们的新增轮播图接口已经实现了预期的效果了,但现在还不是结束这一小节的时候,我们的代码还存在一些瑕疵。首先让我们回想一下我们在第一章中关于RESTful API最佳实践的内容,我们刚刚在成功新增了一个轮播图之后,后端并没有返回任何信息,虽然HTTP状态码为200,但我们依然不得不通过查询所有轮播图接口来确认本次的新增操作是否成功,这是不符合规范的。那么这里我们先来着手解决这个问题,回到我们项目代码中,定位到Banner控制器下的addBanner()方法:

<?php


namespace app\api\controller\v1;

use app\api\model\Banner as BannerModel;
use think\facade\Request;

class Banner
{
    ...........

    public function addBanner()
    {
        $params = Request::post();
        BannerModel::add($params);
        return writeJson(201,[],'新增成功!');

    }

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

这里我们调用了lin-cms-tp5提供的一个公共方法writeJson(),第一个参数是设置要返回的HTTP状态码,第二个参数是要返回的数据,第三个参数是提示信息。这里我们由于是新增了一个资源(轮播图),按照RESTful API的最佳实践,我们需要返回给前端一个201的状态码顺便提供了一个清晰的提示。

现实开发中,某些接口因为业务需求原因需要在新增某个资源后返回所新增的内容,这时候可以把这部分内容传递给writeJson()方法的第二个参数

回到Postman中,还是新增轮播图接口,直接发送请求,得到结果:

可以看到Postman中提示本次请求的返回结果的Status是201 Created,返回的数据是一个JSON字符串:

{
    "error_code": 0,
    "result": [],
    "msg": "新增成功!"
}

1
2
3
4
5
6

这下子我们就很清楚的知道我们请求是否已经被正确执行了,也许你这个时候还不放心还想再确认一下是否真的添加了,那么就请读者自行再次调用一下查询所有轮播图接口,这里就不作演示了,因为我很放心。但是,是不是无论提交了什么数据都应该如此顺利的插入到数据库中呢?答案是否定的!我们要保证的是那些经过校验的、安全的数据顺利插入到我们的数据库中,反之我们应该拒绝它。目前新增轮播图接口并没有做任何的数据校验工作,这个接口是存在风险的,我们马上来解决这个问题。

<?php


namespace app\api\controller\v1;

use app\api\model\Banner as BannerModel;
use think\facade\Request;

class Banner
{
    ...........

    /**
     * 新增轮播图接口
     * @validate('BannerForm')
     * @return \think\response\Json
     */
    public function addBanner()
    {
        $params = Request::post();
        BannerModel::add($params);
        return writeJson(201,[],'新增成功!');

    }

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

addBanner()方法上,我们添加了一段注解内容@validate('BannerForm'),使用了注解验证器

这是lin-cms-tp5在TP5框架验证器功能的基础上实现的,在保留了原有功能的同时提供了更优雅的调用方式。更多关于注解验证器的介绍和使用方法点击查看(opens new window)

添加了这段注解内容之后,每次调用这个接口时,框架在正式执行控制器方法的内容之前会先调用@validate(),如果校验通过则开始执行控制器方法的内容,如果校验不通过则会抛出异常信息。这里我们给他传递了一个叫BannerForm的验证器类,接着我们就来创建一下这个类。根据lin-cms-tp5的目录结构规范,我们在项目根目录下的application/api/validate创建一个新的banner文件夹,接着右键这个文件夹,在弹出菜单中依次选择New——PHP Class,Name写上BannerForm。lin-cms-tp5已经为我们封装好了全局统一的异常处理机制,我们在自定义验证器的时候,只需要继承一个BaseValidate 类,剩下的就是和TP5框架使用自定义验证器的方法一致:

<?php


namespace app\api\validate\banner;


use LinCmsTp5\validate\BaseValidate;

class BannerForm extends BaseValidate
{
// {
// 	   "name": "LinCMS",
//     "description": "第一个新增的轮播图",
//     "items":[
//         {
//             "img_id": 65,
//             "key_word": "6",
//             "type": 1
//         },
//         {
//             "img_id": 65,
//             "key_word": "6",
//             "type": 1
//         }
//     ]
// }
    protected $rule = [
        'name' => 'require',
        'items' => 'require|array|min:1'
    ];

    protected $message = [
        'name' => '轮播图名称不能为空',
        'items.require' => '轮播图元素不能为空',
        'items.array' => '轮播图元素的值必须为数组'
    ];
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37

在继承BaseValidate后这个类就成为了自定义验证器类,通过定义成员变量$rule指定要校验的字段和校验规则,校验规则可以使用正则表达式、自定义规则或者TP5框架提供的内置规则(opens new window) ,这里的require表示这属于一个调用接口时必传的字段,这里我们要求当调用新增轮播图接口时,轮播图的名称和描述以及包含的元素都必须传递,其中轮播元素字段还要求是一个数组且长度至少为1(多个规则之间以|分隔)。

通过定义成员变量$message来指定当字段校验不通过时所显示的错误提示信息,这里不定义的话会采用TP5框架提供的默认提示信息。当一个字段有多个校验规则时,可以通过字段名+.规则名指定不同规则校验不通过时要显示的信息。

定义完验证器后,我们回到Postman中,修改一下我们之前提交的数据内容:

{
	"name": "LinCMS",
    "description": "第一个新增的轮播图"
}
1
2
3
4

这里只传递轮播图名称和描述,把轮播元素去掉,然后发送请求,这时候你会发现Postman已经显示本次请求后端返回了一个400的HTTP状态码,并返回了以下JSON信息:

{
    "msg": {
        "items": "轮播图元素不能为空"
    },
    "error_code": 99999,
    "request_url": "POST /v1/banner"
}

1
2
3
4
5
6
7
8

我们再尝试把轮播图的描述字段也去掉后点击发送请求,得到如下结果:

{
    "msg": {
        "description": "轮播图描述不能为空",
        "items": "轮播图元素不能为空"
    },
    "error_code": 99999,
    "request_url": "POST /v1/banner"
}
1
2
3
4
5
6
7
8

最后我们尝试下给items传递一个字符串或者数字,得到如下结果:

{
    "msg": {
        "items": "轮播图元素的值必须为数组"
    },
    "error_code": 99999,
    "request_url": "POST /v1/banner"
}
1
2
3
4
5
6
7

经过简单的测试表明,我们的验证器已经正常运行了,那么这几次测试的请求数据有没有插入到数据库中呢?答案当然是没有的,不信你可以调用下查询所有轮播图的接口验证一下。

TP5框架的验证器功能非常强大,多数情况下内置规则已经能满足业务需求,如不能满足还可以自定义规则。这里读者可以尝试使用内置规则限制name或者description字段提交的内容必须是中文

一阵忙活,我们优化完善了控制器层的数据校验和返回信息,那么模型层是否也有需要优化完善的地方呢?让我们打开模型层下的Banner模型,定位到add()方法检查检查:

    public static function add($params)
    {
        // 调用当前模型的静态方法create(),第一个参数为要写入的数据,第二个参数标识仅写入数据表定义的字段数据
        $banner = self::create($params, true);
        // 调用关联模型实现关联写入
        $banner->items()->saveAll($params['items']);

    }
1
2
3
4
5
6
7
8

前面我们在控制层已经做了相应的数据校验,模型层这里直接执行数据插入,看起来好像没什么问题,但是万一插入过程有问题呢?打比方我们正在新增一个轮播图,当self::create($params, true)代码执行完毕了,正准备执行下面的关联新增的时候,另一个人刚好删除了banner表中刚刚新增出来的记录,这时候你就会发现你数据库中banner表和banner_item表的数据对应不上了。还有一种情况就是,banner表的记录已经创建,在执行关联新增的时候,框架发现你传递的字段内容太长了,写入失败,这时候你会发现banner表有数据,但是banner_item表里并没有与之对应的记录。这里的问题就是我们没有考虑关联写入时的数据一致性问题,知道了问题所在,我们就来着手解决下这个问题:

<?php


namespace app\api\model;


use app\lib\exception\banner\BannerException;
use think\Db;
use think\Exception;
use think\Model;

class Banner extends Model
{

    /**
     * @param $params
     * @throws BannerException
     */
    public static function add($params)
    {
        // 启动事务
        Db::startTrans();
        try {
            // 调用当前模型的静态方法create(),第一个参数为要写入的数据,第二个参数标识仅写入数据表定义的字段数据
            $banner = self::create($params, true);
            // 调用关联模型实现关联写入
            $banner->items()->saveAll($params['items']);
            // 提交事务
            Db::commit();
        } catch (Exception $ex) {
            // 回滚事务
            Db::rollback();
            throw new BannerException([
                'msg' => '新增轮播图失败',
                'error_code' => 70001
            ]);
        }

    }

    public function items()
    {
        return $this->hasMany('BannerItem');
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45

这里我们使用TP5框架内置的Db::startTrans()来启动一个数据库事务,然后用try/catch包裹原来关联新增的代码片段,当数据新增过程没有异常时,最后执行Db::commit(),发生异常时,执行Db::rollback()并抛出一个自定义异常信息。Db::startTrans()可以让后续出现数据库操作结果都暂时“保存”起来但不提交到数据库,并且让同一时间其他用户的相同请求暂时阻塞,直到代码运行到Db::commit()才会提交所有操作到数据库并释放阻塞;反之,如果执行到了Db::rollback()则会回滚前面已经执行的数据库操作。这就是事务操作的好处,它可以保证你多个不同的数据库操作要么都执行,要么都不执行,这样就保证了两张表的数据一致性。同时结合try/catch的机制,我们让整个关联新增过程变得更加可控和可定制化,比如说这里我们在catch到异常的时候抛出了一个自定义异常,在一些比较特殊业务场景中,我还可以添加日志记录功能供我们后期定位解决问题。

当执行过程中出现异常时,按照RESTfull API的最佳实践,我们需要给前端返回一个明确的错误信息,这里我们利用lin-cms-tp5封装的异常处理机制,自定义了一个BannerException异常类,接下来我们就来创建这个类,在项目根目录下 的application/lib/exception下新建一个banner文件夹,然后右键这个文件夹,在弹出菜单中依次选择New——PHP Class,输入类名BannerException,并加入以下代码:

自定义异常类需要继承lin-cms-tp5提供的BaseException类,同时定义三个成员变量并给定初始值,$code为异常抛出时的HTTP状态码,$msg是异常的提示信息,$error_code是自定义错误码。定义完自定义异常类之后,我们就可以在任何地方使用throw new 异常类名()的方式去抛出这个异常,如果需要覆盖自定义异常类成员变量的值,就需要传递一个数组给自定义异常类,数组的键对应成员变量的变量名,值为你想显示的内容:

throw new BannerException([
                'msg' => '新增轮播图失败',
                'error_code' => 70001
            ]);
1
2
3
4

这里我们就覆盖了msg和error_code的初始值,现在让我们来做个测试,我们人为的让关联新增的过程抛出异常:

<?php


namespace app\api\model;


use app\lib\exception\banner\BannerException;
use think\Db;
use think\Exception;
use think\Model;

class Banner extends Model
{

    /**
     * @param $params
     * @throws BannerException
     */
    public static function add($params)
    {
        // 启动事务
        Db::startTrans();
        try {
            // 调用当前模型的静态方法create(),第一个参数为要写入的数据,第二个参数标识仅写入数据表定义的字段数据
            $banner = self::create($params, true);
            // 人为制造异常
            1/0;
            // 调用关联模型实现关联写入
            $banner->items()->saveAll($params['items']);
            // 提交事务
            Db::commit();
        } catch (Exception $ex) {
            // 回滚事务
            Db::rollback();
            throw new BannerException([
                'msg' => '新增轮播图失败',
                'error_code' => 70001
            ]);
        }

    }

    public function items()
    {
        return $this->hasMany('BannerItem');
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47

这里我们在新增banner记录的操作之后加入了一行会导致程序异常的代码,接着回到Postman,打开新增轮播图接口,按照前面的操作加入相关的参数然后发送请求:

{
    "msg": "新增轮播图失败",
    "error_code": 70001,
    "request_url": "POST /v1/banner"
}
1
2
3
4
5

这时候我们发现后端给我们返回了一个HTTP状态为400的结果,并提供了一段JSON数据,这里说明我们的自定义异常类已经产生作用。那么我们的事务有没有生效呢?点击左边获取所有轮播图接口并发送请求,你会发现并没有新增的记录,这说明我们的事务也成功生效了。

按照RESTfull API的最佳实践,当后端产生异常时,我们需要返回格式统一且清晰的错误提示给前端,lin-cms-tp5提供的BaseException类就是负责帮我们捕获异常并封装统一的异常信息格式,我们通过继承这个类来实现功能复用。

到这里,我们的新增轮播图接口才算是完成了,在原来实现基本功能的基础上,我们增加了数据校验事务机制正常/异常返回结果处理,在结束本章节之前,记得删除刚刚人为制造异常的代码。

思考题:
上一小节我们实现了查询所有轮播图的接口,当查询结果是空的时候,我是不是要考虑给个异常提示呢?如果需要,那么请你动手实现一个吧!

扩展知识:
我们在开发接口的时候,实现功能是目标,但不能仅仅是实现目标而已,我们要需要让整个过程是可靠、可控、可追溯。对于前端来说,完善的数据校验机制和异常处理机制可以减少很多不必要的沟通和对接成本,因为接口的返回结果已经清晰的告诉你了;对于后端来说,也有利于缩小故障范围快速定位问题。

# 删除轮播图

在上一小节中,我们学习了如何新增轮播图,我们在测试过程中我们创建了几个轮播图,也许读者在前面的学习过程中遇到了一些小问题或者喜欢上了新建轮播图的感觉,实际创建了更多的轮播图。由于是测试数据,很多都是重复的,这时候我们不想要了,该怎么办?不要想着操作数据库,我们来动手开发一个删除轮播图的接口。

在控制层目录下打开我们熟悉的Banner控制器类,新增一个delBanner()控制器方法并添加如下代码:

<?php


namespace app\api\controller\v1;

use app\api\model\Banner as BannerModel;
use app\lib\exception\banner\BannerException;
use think\facade\Request;

class Banner
{

    public function getBanners(){...}

    public function addBanner(){...}

    public function delBanner()
    {
        $ids = Request::delete('ids');
        array_map(function ($id) {
            // 查询指定id的轮播图记录
            $banner = BannerModel::get($id,'items');
            // 指定id的轮播图不存在则抛异常
            if (!$banner) throw new BannerException(['msg' => 'id为' . $id . '的轮播图不存在']);
            // 执行关联删除
            $banner->together('items')->delete();
        }, $ids);
        return writeJson(201, [], '轮播图删除成功!');
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

这里同样先通过TP5框架内置的Request类来获取前端提交过来的参数,这里我们约定前端需要提交一个ids的参数,参数的值是一个由一个或多个id组成的数组,这样可以同时兼容单个删除或者批量删除的需求,只要前端封装好请求参数的数组即可。同时为了加强下接口安全性,这里我们也明确地给delete()方法指定了要获取的参数名。

获取到参数后,我们利用了PHP内置的array_map函数来实现为每一个$ids元素作用一个函数,函数的内容就是查询指定id的轮播图然后删除,这里采用TP5框架模型内置的关联删除。首先通过模型的get()方法查询出对应的banner实例,注意这里我们除了要传入主键id之外,还需要传入定义模型关联的方法名,也就是我们前面用到过多次的items。拿到banner实例之后,我们就可以通过调用$banner->together('items')->delete()来实现关联删除。但这里代码还没有写完,如果我们这里就开始调用并测试接口你会发现记录确实删除了,而且是直接从表中直接把记录移除了,这是一个硬删除(物理删除)操作,这里我们更推荐的做法是做一个软删除软删除不会从表中真正的删除记录,而是通过标识使得这条记录在系统逻辑层面上不可见。TP5框架要实现数据库软删除的方法很简单,打开我们的Banner模型类,在之前定义$hidden的地方,加入use SoftDelete即可,如:

<?php


namespace app\api\model;


use app\lib\exception\banner\BannerException;
use think\Db;
use think\Exception;
use think\Model;
use think\model\concern\SoftDelete;#引入SoftDelete的命名空间

class Banner extends Model
{
    use SoftDelete;
    protected $hidden = ['delete_time','update_time'];
    
    ...............
    ...............
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

注意这里需要引入SoftDelete的命名空间,

使用同样的方式在BannerItem模型中也添加一个use SoftDelete。Banner模型和BannerItem模型都添加完软删除的定义之后,我们就可以来给这个接口定义一条路由了,打开我们的路由配置文件route.php,新增一条路由规则:

Route::group('', function () {
    Route::group('cms', function () {
        // CMS管理相关的路由规则
        // 内容省略。。。。
    });
    Route::group('v1', function () {
        // 业务接口相关的路由规则
        Route::group('banner',function(){
            Route::get('','api/v1.Banner/getBanners');
            Route::post('','api/v1.Banner/addBanner');
            Route::delete('','api/v1.Banner/delBanner');
        });
    });
})->middleware(['Auth','ReflexValidate'])->allowCrossDomain();
1
2
3
4
5
6
7
8
9
10
11
12
13
14

这里我们新增了一条请求类型为delete的路由,地址不变,路由到Banner控制类下面的delBanner()方法,路由定义完毕我们就来Postman中测试一下。打开Postman,我们在左边的分组目录下新建一个名叫删除轮播图的请求,并修改请求类型和填写我们的路由地址:

创建完请求后,我们就来利用Postman模拟下前端的删除轮播图操作,按下图所示配置下请求参数:

根据自己banner表中的实际情况填写要删除的id即可。

这里我们模拟了要删除id为3、4、5的轮播图,如果只想删除一个,就写[1]即可,但要保证ids参数的值是一个数组。点击发送:

{
    "error_code": 0,
    "result": [],
    "msg": "轮播图删除成功!"
}
1
2
3
4
5

没有报错,提示我们删除成功了。这时候我们打开phpMyAdmin去查看下我们的banner表和banner_item表:

这里我们发现,表中的记录并没有消失,取而代之的是delete_time字段原来的空值现在变成了一个时间戳,这就说明我们的软删除已经实现了,这时候你再尝试在Postman中调用查询所有轮播图接口你会发现只查询出了id为1和2的轮播图,3,4,5已经不见了。

到这里我们删除轮播图功能的接口就已经基本实现了,但还不是进入下一小节的时候,因为有些工作我们还没完善好。回到我们的项目代码中Banner控制器类下的delBanner()方法,在前一小节中我们提到了数据校验对于数据插入的重要性,由于该接口是删除操作,为了安全性和程序的稳定性,我们同样需要为其实现一个数据校验,这里给读者介绍lin-cms-tp5中注解验证器的另一种使用方法:

<?php


namespace app\api\controller\v1;

use app\api\model\Banner as BannerModel;
use app\lib\exception\banner\BannerException;
use think\facade\Request;

class Banner
{

    public function getBanners(){...}

    public function addBanner(){...}

    /**
     * @param('ids','待删除的轮播图id列表','require|array|min:1')
     */
    public function delBanner()
    {
        $ids = Request::delete('ids');
        array_map(function ($id) {
            // 查询指定id的轮播图记录
            $banner = BannerModel::get($id,'items');
            // 指定id的轮播图不存在则抛异常
            if (!$banner) throw new BannerException(['msg' => 'id为' . $id . '的轮播图不存在']);
            // 执行关联删除
            $banner->together('items')->delete();
        }, $ids);
        return writeJson(201, [], '轮播图删除成功!');
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33

这里我们通过@param()指定了一个数据校验,要求ids参数是必传的,而且是长度最小为的数组1。

注解验证器中的参数校验功能适用于简单校验场景,无需单独创建一个自定义验证器类。

接下来我们来验证一下,打开Postman,修改一下我们的ids参数,这里我们让它传递一个空数组:

点击发送请求:

{
    "msg": {
        "ids": "待删除的轮播图id列表不能为空"
    },
    "error_code": 99999,
    "request_url": "DELETE /v1/banner"
}
1
2
3
4
5
6
7

验证器已经生效,其他情况的传参读者可以自行测试。在完成了数据之后,一切似乎都已经准备妥当,但事实上并不是,目前接口虽然做了数据校验,但是这里要思考一个问题,对于删除操作,数据合法就得让他删除么?显然不是。试想你今天辛辛苦苦创建了好几个轮播图,上个厕所回来都没了,个个都表示不是他干的,所以,对于敏感操作的接口,我们还需要引入接口权限行为日志的功能。

我们先来实现行为日志的记录功能,在lin-cms-tp5中,要实现行为日志非常简单,只需要在需要记录日志的地方加入一段代码,如我们的delBanner()控制器方法:

<?php


namespace app\api\controller\v1;

use app\api\model\Banner as BannerModel;
use app\lib\exception\banner\BannerException;
use think\facade\Hook; # 引入Hook类
use think\facade\Request;

class Banner
{

    public function getBanners(){...}

    public function addBanner(){...}

    /**
     * @param('ids','待删除的轮播图id列表','require|array|min:1')
     */
    public function delBanner()
    {
        $ids = Request::delete('ids');
        array_map(function ($id) {
            // 查询指定id的轮播图记录
            $banner = BannerModel::get($id,'items');
            // 指定id的轮播图不存在则抛异常
            if (!$banner) throw new BannerException(['msg' => 'id为' . $id . '的轮播图不存在']);
            // 执行关联删除
            $banner->together('items')->delete();
        }, $ids);
        // 记录本次行为的日志
        Hook::listen('logger', '删除了id为' . implode(',', $ids) . '的轮播图');
        return writeJson(201, [], '轮播图删除成功!');
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36

我们在return之前加入了Hook::listen('logger', '删除了id为' . implode(',', $ids) . '的轮播图');这段代码,listen()方法接收两个参数,第一个参数是行为的名称,logger是lin-cms-tp5内在封装好的行为,此为固定值。第二个参数是一个字符串,即要记录的日志内容。

在测试行为日志功能之前,我们需要在Postman中配置一些参数,因为行为日志功能会通过解析请求中携带的令牌信息来获得调用者的身份信息,在正常与lin-cms-vue搭配的使用过程中,cms会给每个请求都携带上令牌信息,但我们在Postman中默认是没有的,我们需要手动配置一下。首先我们要先拿到一个有效的令牌,访问我们前面已经安装部署好的lin-cms-vue,在登陆到首页之后,按下键盘的F12(不同浏览器快捷键可能不同),呼出浏览器的开发者工具,然后切换到Network标签。

接着我们随便找一个会向后端发起请求的页面,这里我们选择左边菜单栏里的日志管理,点击后会发现Network记录到了一些请求记录,我们点击其中一个状态码为200的请求记录,这时候会显示这个这个请求的详细信息,其中在Request Headers里,有个Authorization信息,这里就是存放令牌的地方,复制Bearer 后面的那一大串字符串,这就是我们要的令牌。

注意这里你会发现每个请求都会发送两次,但请求类型不同,一个是OPTIONS,一个是GET或者其他,对应我们定义的路由规则。这里需要选择请求类型与路由定义一致的类型才会看到你想要的东西。

接着回到Postman中,点击地址栏下方的Authorization标签,TYPE选项这里,选择Bearer Token,选择后右边会显示一个token输入框,在输入框中粘贴进去我们刚刚复制的令牌。

这样我们就模拟了一个携带令牌的请求,要注意这个令牌是有过期时间的,如果接口提示令牌过期,那么读者就按前面的操作方式,在lin-cms-vue中登陆下然后复制个令牌替换Postman原来的令牌信息。接着我们修改下我们的请求参数,再删除一次轮播图:

{
    "error_code": 0,
    "result": [],
    "msg": "轮播图删除成功!"
}
1
2
3
4
5

没有报错,接下来我们来验证一下是否记录了本次的删除行为,回到lin-cms-vue中,刷新一下我们刚刚打开的日志管理页面,我们可以看到这里已经多出了一条关于删除轮播图的行为日志记录:

除了我们刚刚的删除轮播图行为日志,读者还可以看到关于登陆的行为日志,lin-cms-tp5默认会给登陆操作也记录行为日志,这样我们就知道每个cms用户在什么时候登陆过。

推荐给每一个敏感操作都记录行为日志,更多关于行为日志的介绍点击查看(opens new window)

到这里我们已经实现了行为日志的记录,我们可以清楚的知道操作都是由什么账户执行的,但这都是事后的。往往很多时候一些操作是应该交给有权限的人来操作,比如说一个营运管理员,他可以查询、创建、编辑轮播图,但是删除要交给主管来做,我们在行为发生之前就应该先阻止掉没有操作权限的用户请求。幸运的是,lin-cms已经为我们实现了一套完整的权限校验机制,接下来我们就来学习如何使用他,回到我们的lin-cms-tp5项目代码中,定位到我们的delBanner()控制器方法:

<?php


namespace app\api\controller\v1;

use app\api\model\Banner as BannerModel;
use app\lib\exception\banner\BannerException;
use think\facade\Hook; # 引入Hook类
use think\facade\Request;

class Banner
{

    public function getBanners(){...}

    public function addBanner(){...}

    /**
     * @auth('删除轮播图','轮播图管理')
     * @param('ids','待删除的轮播图id列表','require|array|min:1')
     */
    public function delBanner()
    {
        $ids = Request::delete('ids');
        array_map(function ($id) {
            // 查询指定id的轮播图记录
            $banner = BannerModel::get($id,'items');
            // 指定id的轮播图不存在则抛异常
            if (!$banner) throw new BannerException(['msg' => 'id为' . $id . '的轮播图不存在']);
            // 执行关联删除
            $banner->together('items')->delete();
        }, $ids);
        // 记录本次行为的日志
        Hook::listen('logger', '删除了id为' . implode(',', $ids) . '的轮播图');
        return writeJson(201, [], '轮播图删除成功!');
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37

在lin-cms-tp5中,要实现接口权限的控制,只需要在控制器方法上的注释中加入@auth('权限名称','权限模块名称')格式的注解,在这里我们给这个权限起了一个名字叫删除轮播图,属于轮播图管理模块。

权限名称和权限模块名称可自定义,但推荐开发者事先规划好,比如说轮播图有新增、查询、编辑,都属于轮播图管理模块。

这样我们就配置好了删除轮播图接口的权限,到这里我们就真正的完成配置了,接下来我们就来验证一下这个权限控制是否真的有效。lin-cms-tp5的权限控制同样需要通过请求中的令牌来获取请求者身份信息,目前我们的令牌是基于super用户,在lin-cms-tp5中,super拥有所有权限,权限校验机制会直接对来自super的请求放行,所以我们需要创建一个没有删除轮播图权限的普通账户,用这个普通账户的令牌来测试我们的权限控制。事不宜迟,打开我们的lin-cms-vue并使用super登陆,登陆后,依次点击左边菜单栏的权限管理——分组管理,我们先创建一个用户分组,后面创建用户的时候会用到。

这里我们创建了一个很流行的分组,在下面分配权限的区域我们可以看到我们刚刚自定义的权限名称和模块,这里不勾选,直接保存,提示添加分组成功后,点击左边菜单栏分组管理上面的用户管理——添加用户。

这里我们创建了一个用户名为小蔡的用户,并把这个用户分配到了我们上一步创建的分组,点击保存。保存成功后,点击右上方头像,选择退出账户,在登录界面输入我们刚刚创建的用户名和密码,登录成功后重复之前我们复制令牌的步骤,把这个用户的令牌信息复制出来并粘贴到Postman中然后调用删除轮播图接口,你会发现这时候接口返回了一个403错误,并提示:

{
    "msg": "权限不足,请联系管理员",
    "error_code": 10000,
    "request_url": "DELETE /v1/banner"
}
1
2
3
4
5

看到这个错误提示就说明我们的权限校验已经生效了,只有超级管理员或者拥有删除轮播图权限的分组用户才能调用这个接口。

扩展知识:
硬删除也叫物理删除,即完全将记录从数据库存储文件中删除。软删除也叫逻辑删除,即通过改变某个字段值来实现系统逻辑层面无法查询,但数据仍保存在数据库存储文件中。软删除的优点显而易见,由于数据还是真实存在于数据库的存储文件中,这意味着你只需要把标识字段改回来即可实现“数据恢复”,而硬删除,一旦删除了,恢复起来就没那么容易了,就好比你删除Shift+Del了一个文件。另外一个就是硬删除后,由于数据不复存在了,这时候如果需要对这张表做数据分析你就等于缺少了这一部分的数据样本,而软删除就不存在这个问题,我们可以简单的通过代码来实现包含这部分已经软删除了的数据查询并统计分析。所以真实项目中,一般都采用软删除。

# 编辑轮播图

前面我们实现了删除轮播图接口来清理我们多余的测试轮播图数据,剩下的轮播图有些内容我们不是很满意想要修改,整个删除然后新建是个办法,但我们不会这么做,本小节我们将学习开发几个关于编辑轮播图的接口,是的,几个,不是一个。这里读者可能会有疑问,编辑轮播图接口按照前面的经验不也是控制器、模型、路由就完事了么?思路确实如此,但是编辑轮播图接口的业务情况稍微有点复杂。我们一个轮播图其实包含了两个部分,一个是主体部分,有轮播图的名称和简介,另一个是轮播元素部分,里面包含了跳转类型和关键字、图片id等内容,我们在尝试编辑一个轮播图的时候,有可能只是编辑主体部分,也有可能只是编辑轮播元素部分,或者两者都存在。而编辑轮播元素部分有可能会出现删除、新增、修改,如果把这几个业务情况都在一个接口里实现,那这个接口就会变得很臃肿,为了实现区分不同的编辑内容我们无可避免的需要做很多判断,前端任何一个小改动后端都需要完整的判断所有内容是否改动过,所以我们就需要把这一个大接口,细分为几个小接口,让前端可以按需调用。这里我们把编辑轮播图接口拆分为编辑轮播图信息新增轮播图元素编辑轮播图元素删除轮播图元素四个接口来实现,打开我们的Banner控制器类,分别创建以下四个控制器方法:

<?php


namespace app\api\controller\v1;

use app\api\model\Banner as BannerModel;
use app\lib\exception\banner\BannerException;
use think\facade\Request;

class Banner
{

    /*查询所有轮播图*/
    public function getBanners(){...}
    /*新增轮播图*/
    public function addBanner(){...}
    /*删除轮播图*/
    public function delBanner(){...}

    /**
     * 编辑轮播图基础信息
     * @param $id
     * @param('id','轮播图id','require|number')
     * @param('name','轮播图名称','require')
     */
    public function editBannerInfo($id)
    {
        $bannerInfo = Request::patch();
        $banner = BannerModel::get($id);
        if (!$banner) throw new BannerException(['msg' => 'id为' . $id . '的轮播图不存在']);
        $banner->save($bannerInfo);
        return writeJson(201, [], '轮播图基础信息更新成功!');
    }

    /**
     * 新增轮播图元素
     */
    public function addBannerItem()
    {
        $data = Request::post('items');

        foreach ($data as $key => $value) {
            BannerItemModel::create($value);
        }
        return writeJson(201, [], '新增轮播图元素成功!');
    }

    /**
     * 编辑轮播图元素
     */
    public function editBannerItem()
    {
        $data = Request::put('items');

        $bannerItem = new BannerItemModel;
        # allowField(true)表示只允许写入数据表中存在的字段。
        # saveAll()接收一个数组,用于批量更新或者新增。通过判断传入的数组中是否设置了id属性,如果有则视为更新,无则视为新增
        $bannerItem->allowField(true)->saveAll($data);
        return writeJson(201, [], '编辑轮播图元素成功!');
    }

    /**
     * 删除轮播图元素
     * @param('ids','待删除的轮播图元素id列表','require|array|min:1')
     */
    public function delBannerItem()
    {
        $ids = Request::delete('ids');
        // 传入多个id组成的数组进行批量删除
        BannerItemModel::destroy($ids);
        return writeJson(201, [], '轮播图元素删除成功!');
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73

这里我们在Banner控制器类下面新增了editBannerInfo()addBannerItem()editBannerItem()delBannerItem(),通过将原来一个比较复杂庞大的接口细分成了几个小接口,每个小接口只负责一件事情,这样做有利于提高接口的复用性和可维护性(哪个操作出问题了,我们就去哪个接口debug,不用面对一堆复杂的逻辑代码),当然从前端角度来说,在页面就需要相应的做一些判断来决定需要调哪些接口,这会增加一些代码量,但是换一个角度,接口的细分和单一性也给了前端组合页面功能更多可能性,这就是我们前面说的按需调用。比如说我们在cms中做了一个轮播图管理页面,点击某个轮播图进入详情页并修改了某个轮播图的名字,这时候后端就得判断一下这次提交有没有新增、有没有删除、有没有修改、修改了什么等等,但其实我们就是改了下轮播图的名字而已。有细分后的接口,这个问题就很好解决了,我们可以在轮播图列表实现快速编辑表单项的功能,当用户在列表页修改了轮播图的名称或者简介,直接调用editBannerInfo()这个接口即可,如果是在详情页,我们就可以通过对比提交前后数据的差异,来决定调用哪些接口。

仔细观察我们刚刚新增了四个控制器方法,读者们可以直观的感受到每个控制器方法的逻辑都非常简单,就是接收参数,模型操作,数据校验、都是在复用我们前面几个小节的知识点而已,当然本小节也有关于新知识点的编码环节,但这个我们稍后再说,在编码环节之前,我们先来认识一下一个新名词——接口粒度。我们做API开发的时候,根据实现方式的不同,代码量和复杂度也不同,比如说我们这里实现的编辑轮播图接口,这里虽然我们细分成了四个接口,但四个接口的代码量和复杂度加起来可能还不到通过一个接口实现的方式来得多,这里就是对接口粒度的一种细分,我们把接口由一个粗粒度接口细分成了多个细粒度的接口。细分接口粒度优点前面已经说了很多,至于缺点就是可能会存在多次调用的情况,多次调用意味着更多的网络带宽开销,但是在这个功能点上,细分后的优点足以盖过缺点。

关于接口粒度的设计和运用在日常开发中经常会用到,有时候可能是有意识也有可能是习惯或者经验,但不管如何,适当的细分接口粒度对开发效率和维护都有很大的帮助。这里通过对一个业务需求的分析让大家有个初步的认识,读者可在后续教程内容和日常开发中加深体会。

在了解了编辑轮播图接口设计的初衷之后,让我们回到项目代码中,这里我们大致的介绍一下各个控制器方法的内容,读者可以对照一下看看是否和自己理解的一致,巩固一下知识点。

  • editBannerInfo()
/**
     * 编辑轮播图基础信息
     * @param $id
     * @param('id','轮播图id','require|number')
     * @param('name','轮播图名称','require')
     */
    public function editBannerInfo($id)
    {
        $bannerInfo = Request::patch();
        $banner = BannerModel::get($id);
        if (!$banner) throw new BannerException(['msg' => 'id为' . $id . '的轮播图不存在']);
        $banner->save($bannerInfo);
        return writeJson(201, [], '轮播图基础信息更新成功!');
    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14

editBannerInfo()用于编辑banner的基础信息,banner表中可以提供修改字段分别为name和description字段,这里我们要求控制器方法接收一个参数$id,用于指定需要修改哪个banner,这种参数的定义方式要求调用者在请求接口url的时候就必须体现在url中,具体形式一会我们在定义路由的时候再介绍。接着是通过Request获取对应请求类型所携带的参数,这里我们把修改轮播图基础信息接口的HTTP请求类型定义为PATCH。这个参数就包含了name或者description的内容。查询出指定id的banner模型实例后,调用模型方法save()更新内容。同时给接口增加关于id和name的参数校验。

  • delBannerItem()
    /**
     * 删除轮播图元素
     * @param('ids','待删除的轮播图元素id列表','require|array|min:1')
     */
    public function delBannerItem()
    {
        $ids = Request::delete('ids');
        // 传入多个id组成的数组进行批量删除
        BannerItemModel::destroy($ids);
        return writeJson(201, [], '轮播图元素删除成功!');
    }
1
2
3
4
5
6
7
8
9
10
11

delBannerItem()用于删除轮播图下的元素,这里我们定义接口接收一个参数名叫ids的参数,参数的值是一个由banner_item表记录id组成的数组,通过传递这个数据给模型的destroy()方法来实现批量删除,由于这里我们是要删除轮播图元素,所以使用的是BannerItem模型,这里我们同样给模型起了个别名BannerItemModel。参数校验我们要求ids参数为必传并且是一个最小长度为1的数组。

  • addBannerItem() & editBannerItem()
    /**
     * 新增轮播图元素
     */
    public function addBannerItem()
    {
        $data = Request::post('items');

        foreach ($data as $key => $value) {
            BannerItemModel::create($value);
        }
        return writeJson(201, [], '新增轮播图元素成功!');
    }

    /**
     * 编辑轮播图元素
     */
    public function editBannerItem()
    {
        $data = Request::put('items');

        $bannerItem = new BannerItemModel;
        # allowField(true)表示只读取表中存在的字段。
        # saveAll()接收一个数组,用于批量更新或者新增。通过判断传入的数组中是否设置了id属性,如果有则视为更新,无则视为新增
        $bannerItem->allowField(true)->saveAll($data);
        return writeJson(201, [], '编辑轮播图元素成功!');
    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

最后是addBannerItem() 和 editBannerItem(),细心的读者会发现这里我们暂时没有给这两个控制器方法添加任何验证器,当然不是说不需要做参数校验,而是校验的方式有些特别。首先这两个接口都会接收一个叫items的参数,参数是一个数组,那么我们第一反应就是像前面一样指定一下参数的类型校验不就可以了?确实可以,但还不够。TP5框架内置的验证规则只能校验参数是不是数组,而我们这里的需求是除了校验是不是数组,我们还要校验数组的内容,而且不同场景(新增、编辑)下,这个验证内容的规则也有点不一样,这就需要我们来自定义验证规则了。前面在新增轮轮播图小节我们学会如创建并使用一个自定义验证器类,但我们只是做了最基础的使用,本小节我们来学习如何使用验证器里的验证场景功能。首先我们需要创建一个自定义验证器类,右键项目根目录下的application\api\validate\banner文件夹——New——PHP Class,类名我们叫BannerItemForm,创建成功后添加如下代码:

<?php


namespace app\api\validate\banner;


use LinCmsTp5\validate\BaseValidate;

class BannerItemForm extends BaseValidate
{

    protected $rule = [
        'items' => 'array|require|min:1',
    ];

    public function sceneEdit()
    {
        return $this->append('items', 'checkEditItem');

    }

    public function sceneAdd()
    {
        return $this->append('items', 'checkAddItem');

    }

    protected function checkAddItem($value)
    {
        foreach ($value as $k => $v) {
            if (!empty($v['id'])) {
                return '新增轮播图元素不能包含id';
            }
            if (empty($v['img_id']) || empty($v['key_word']) || empty($v['type']) || empty($v['banner_id'])) {
                return '轮播图元素信息不完整';
            }
        }
        return true;
    }

    protected function checkEditItem($value)
    {
        foreach ($value as $k => $v) {
            if (empty($v['id'])) {
                return '轮播图元素id不能为空';
            }
            if (empty($v['img_id']) || empty($v['key_word']) || empty($v['type'])) {
                return '轮播图元素信息不完整';
            }
        }
        return true;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53

这里我们同样继承BaseValidate类,然后定义了一条items参数的验证规则,要求这个参数的值必须传而且是最小长度为1的数组,接下来,我们定义了两条自定义验证规则——checkAddItem()和checkEditItem(),规则的内容就是遍历参数的值,判断值是否存在或者合法。从方法名称我们可以看出,这是分别对应新增轮播图元素和编辑轮播图元素时用的,那么怎么让这两条规则在不同场景下生效呢?方法就是上面的sceneEdit()和sceneAdd()(注:此为固定命名格式,scene+场景名,场景名首字母大写),这两个方法定义了两个场景,分别是edit和add,每个场景内都执行了一个return $this->append('参数名', '规则名')这段代码,这段代码的作用就是,当这个场景触发时,就给指定的参数名追加一条规则。这里我们默认只有对items参数的长度和类型做校验,当触发了edit或者add场景时,对应的自定义规则就会被追加进去。

这里仅为示范如何使用验证场景功能来解决验证需求,自定义验证规则内容并未做深入实现,读者可以自行补充实现。更多关于TP5验证场景的介绍点击查看(opens new window)

自定义规则和场景我们定义好了,那么怎么触发场景呢?答案当然就是在控制器方法中。

    /**
     * 新增轮播图元素
     * @validate('BannerItemForm.add')
     * @return \think\response\Json
     * @throws \LinCmsTp5\exception\ParameterException
     */
    public function addBannerItem()
    {
        $data = Request::post('items');

        foreach ($data as $key => $value) {
            BannerItemModel::create($value);
        }
        return writeJson(201, [], '新增轮播图元素成功!');
    }

    /**
     * 编辑轮播图元素
     * @validate('BannerItemForm.edit')
     * @throws \Exception
     */
    public function editBannerItem()
    {
        $data = Request::put('items');

        $bannerItem = new BannerItemModel;
        # allowField(true)表示只读取表中存在的字段。
        # saveAll()接收一个数组,用于批量更新或者新增。通过判断传入的数组中是否设置了id属性,如果有则视为更新,无则视为新增
        $bannerItem->allowField(true)->saveAll($data);
        return writeJson(201, [], '编辑轮播图元素成功!');
    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31

触发对应场景的方式与前面我们使用注解验证器的方法大致相同,不同的是我们在注解中的自定义验证器类名后面加上.场景名,通过这种方式,就可以触发对应的场景验证了。

到这里,我们对实现编辑轮播图的四个控制器方法已经充分了解并完善了一些特殊的参数校验,是时候进行最后一步定义路由了,打开route.php文件,添加四条路由规则:

Route::group('', function () {
    Route::group('cms', function () {
        // CMS管理相关的路由规则
        // 内容省略。。。。
    });
     // 业务接口相关的路由规则
    Route::group('v1', function () {
        // 轮播图管理相关接口
        Route::group('banner',function(){
            // 查询所有轮播图
            Route::get('','api/v1.Banner/getBanners');
            // 新增轮播图
            Route::post('','api/v1.Banner/addBanner');
            // 删除轮播图
            Route::delete('','api/v1.Banner/delBanner');
            // 编辑轮播图主体信息
            Route::patch(':id','api/v1.Banner/editBannerInfo');
            // 新增轮播图元素
            Route::post('item','api/v1.Banner/addBannerItem');
            // 编辑轮播图元素
            Route::put('item','api/v1.Banner/editBannerItem');
            // 删除轮播图元素
            Route::delete('item','api/v1.Banner/delBannerItem');
        });
    });
})->middleware(['Auth','ReflexValidate'])->allowCrossDomain();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

还记得我们之前在定义editBannerInfo()方法的时候,我们要求给方法传递一个$id参数,这个参数怎么来的呢?就是在给这个控制器方法定义路由规则的时候,在路由规则上定义个:id,以:开头的参数都表示动态变量,并且会自动绑定到操作方法的对应参数。这样我们的接口调用地址就会变成http://localhost:8000/v1/banner/7,这里的7对应的就是路由规则里的:id,editBannerInfo($id)里接收到的$id值就会是7,url最后的值改变,传递到控制器中的值也会不一样。这种定义方式的好处就是可以加强url的可读性,也可以简化控制器方法参数获取和增强代码可读性。像这种带有动态变量的路由规则叫动态地址。

定义完路由之后,我们就可以到Postman中来测试这几个接口了,还是跟之前一样,我们先在Postman左侧的分组目录中创建这几个请求并设置好请求类型。

具体测试环节和前面的基本大同小异,提醒一点的是在Postman中,如果HTTP的请求类型是PATCH、PUT,或者提交的参数里有数组,那么Body的提交方式要选raw,并且指定格式为JSON(application/json),类似的操作前面我们已经操作过,这里解释说明一下。

首先是编辑轮播图主体信息接口,这里我们尝试修改id为7的轮播图基础信息:

接着是新增轮播图元素接口,这里我们给接口传递一个json格式的参数,为id为10的轮播图新增一个轮播元素:

然后是编辑轮播图元素接口,同样给接口传递一个json格式的参数,修改id为24的轮播元素:

最后是删除轮播图元素接口,这里我们尝试删除id为1的轮播元素:

因为是在Postman中模拟前端应用发起请求需要这么操作,实际与lin-cms-vue搭配使用过程中无需关注这些问题,lin-cms-vue会根据不同请求类型和内容做相应处理。

# 章节回顾

通过本章节的学习我们实现了对轮播图的管理,虽然本质上只是对数据库中两张表的增、删、改、查,但是在除了实现基本功能以外,我们还利用了lin-cms-tp5内置的一些特性实现了诸如统一的异常处理、参数验证、接口权限控制等功能,同时穿插了一些开发思路讲解和知识点介绍,这些内容都是广泛应用于实际开发中的,可以说是干货满满,相信通过本章节的学习,读者已经能够基本掌握lin-cms-tp5的开发流程和技巧,在接下来的章节中,我们同样会延续本章节的风格,让读者除了实现代码还能有更多的收获。

最后更新: 2021-08-12 13:31:59
0/140
评论
0
暂无评论
  • 上一页
  • 首页
  • 1
  • 尾页
  • 下一页
  • 总共1页